
local socket = require'socket'
local print = print
local string = string
local table = table
local pairs = pairs
local ipairs = ipairs
local setmetatable = setmetatable
local assert = assert
local pcall = pcall
local unpack = unpack
local select = select
local type = type
local tonumber = tonumber
--local io = io

module(...)

--variaveis locais
local tab_sock = {}
local map_serv = {}
local map_itf  = {}
local param_aux_tab = {}
local free_port = 55000
local serv_ip
local n_servs 
local max_connections_number = 3

--variaveis globais (que podem ser acessadas fora do modulo)
n_reconnecting = 0
max_attempts = 10
receive_time_out = 0.5

--funcoes locais

local function get_nparam (arg_tab)
  if not param_aux_tab[arg_tab] then
    local nin = 0
    for i,v in ipairs(arg_tab) do
      if v.direction == 'in' or v.direction=='inout' then
        nin = nin+1
      end
    end
    param_aux_tab[arg_tab] = nin
  end
  return param_aux_tab[arg_tab]
end

-- para funcionar corretamente a conexao deve ter
-- timeout = 0, caso contrario o programa pode ficar
-- bloqueado na funcao
local function receive (c)
  socket.select({c},nil,receive_time_out)
  local tab={},str,err
  repeat
    str,err = c:receive() 
    if str then
      table.insert(tab,str)
    end
  until err
  return tab[1] and tab,err
end

local function receive_param (c)
  local param,err = receive(c)
  if param then
    local method = param[1]
    local itf = map_itf[map_serv[c]]
    local n
    for i,m in ipairs(itf.methods) do
      if m.name == method then
        n = get_nparam(m.args)
      end
    end
    if #param ~= n+1 then
      local param2,err2 = receive(c)
      if param2 then
        for i,v in ipairs(param2)do
          table.insert(param,v)
        end
      end
      err = err2
    end
  end
  return param,err
end

local function send (c,str)
  assert(str)
  return c:send(str .. '\n')
end

local function connect (IP,port,attempts)
  assert(IP,port)
  attempts = attempts or 1
  local c,err
  for i=1,attempts do
    c,err = socket.connect(IP,port)
    if c then 
      return c,nil
    end
    print('reconnecting ...')
    socket.sleep(1)
    n_reconnecting=n_reconnecting+1
  end
  return nil,'failed after '.. attempts..': '..err
end

local function unpack_results_aux (map,id,...)
  if not map[id] then return end
  local elem = select(id,...)
  if elem=='nil' then
    elem=nil
  elseif map[id]=='double' then
    elem=tonumber(elem)
  end
  return elem,unpack_results_aux(map,id+1,...)
end

local function unpack_results (map,tab)
  return unpack_results_aux(map,1,unpack(tab))
end

-- assumindo que nil pode ser passado no lugar de qualquer tipo
local function check_types_aux (map,id,...)
  if not map[id] then return end
  local elem = select(id,...)
  if elem==nil then
    elem='nil'
  else
    local msg = 'argumento #'..id..' incorreto, deveria ser '..map[id]..' em vez de '..type(elem)
    assert(not (map[id]=='double') or type(elem)=='number',msg)
    assert(not (map[id]=='string') or type(elem)=='string',msg)
  end
  return elem,check_types_aux(map,id+1,...)
end

local function check_arg_types (map,...)
  return {check_types_aux(map,1,...)}
end

local function create_index_func (IP, port, interface,close)
  -- uma variavel de conexao por proxy
  local c
  return function (tab, key)
    if key=='release' then
      return function ()
        if c then
          c:close()
        end
        c=nil
      end
    end
    local method
    for i,v in ipairs(interface.methods) do 
      if v.name == key then
        method = v
      end
    end
    assert(method,"metodo "..key.." nao esta presente na interface.")
    local nin = 0
    local map_argtype = {}
    local map_rettype = {}
    for i,v in ipairs(method.args) do
      if v.direction == 'in' or v.direction=='inout' then
        table.insert(map_argtype,v.type)
        nin = nin+1
      end
      if v.direction == 'out' or v.direction=='inout' then
        table.insert(map_rettype,v.type)
      end
    end
    tab[key] = function (...)
      local args = check_arg_types(map_argtype,...)
      while #args < nin do
        table.insert(args,'nil')
      end
      table.insert(args,1,key)
      local str = table.concat(args,'\n')
      local tab_ret,err=nil,''
      for i=1,max_attempts do
        if err=='closed' then
          c:close()
          c=nil
        end
        if not c then
          c = assert(connect(IP,port,10))
          c:setoption('tcp-nodelay',true)
          c:settimeout(0)
          --print('closed or nil',c)
        end
        local n_sent,send_error = assert(send(c,str))
        tab_ret,err = receive(c,0.5)
        if tab_ret then
          if close then
            c:close()
            c = nil
            --print('closing',c)
          end
          local success = table.remove(tab_ret,1)
          assert(success=='0',tab_ret[1])
          return unpack_results(map_rettype,tab_ret)
        end
      end
      assert(false,'failed!')
    end
    return tab[key]
  end
end

local function check_servant (s,i)
  print('check_servant',s,i)
  --Verifica se o numero de conexoes ao numero maximo permitido
  if #tab_sock - n_servs == max_connections_number then
    local removeConn = table.remove(tab_sock, n_servs+1)
    print('removing connection',removeConn)
    map_serv[removeConn] = nil
    removeConn:close()
  end
  s:settimeout(0)
  local c = s:accept()
  if c then
    print('new connection',c)
    table.insert(tab_sock,c)
    map_serv[c] = map_serv[s]
    c:setoption('tcp-nodelay',true)
    c:settimeout(0)
  end
  return i+1
end

-- retorna o nome do metodo e uma tabela com os parametros
-- ainda no formato de string
local function getparams(str)
  local tab = {}
  local i = 1
  local method = string.match(str, "(%a%w*)\n")
  for w in string.gmatch(str,"\n([^\n]+)") do
    table.insert(tab,w)
  end
  return method,tab
end

local function check_connection (c,i)
  local inc = 1
  local params,err = receive_param(c)
  print('check_connection',c,i,err)
  --recebeu uma chamada de metodo
  if params then
    --local method,params = getparams(str)
    local received_str = table.concat(params,' ')
    local method = table.remove(params,1)
    --print('method=',method)
    local serv = map_serv[c]
    local res_str
    if serv[method] then
      --convertendo os paramatros para seus respectivos tipos
      local id = 1
      local nouts=0
      for i,m in ipairs(map_itf[serv].methods) do
        if m.name == method then
          for ia, arg in ipairs(m.args) do
            if arg.direction == 'in' or arg.direction=='inout' then
              if params[id] == 'nil' then
                params[id]=nil
              elseif arg.type == 'double' then
                params[id] = tonumber(params[id])
              end
              id = id+1
            end
            if arg.direction == 'out' or arg.direction=='inout' then
              nouts = nouts+1
            end
          end
          break
        end
      end
      local res_tab = {pcall(serv[method],unpack(params))}
      for i=1,nouts do
        if res_tab[i] == nil then
          res_tab[i]='nil'
        end
      end
      if not res_tab[1] then
        local stats = ' received: '..received_str .. ' err: '..err
        --table.insert(res_tab,stats)
        res_tab[2] = res_tab[2] and (res_tab[2] ..stats) or stats
      end
      res_tab[1] = res_tab[1] and 0 or 1
      res_str = table.concat(res_tab,'\n')
    else
      res_str = '1\nmetodo inexistente.'
      print('metodo inexistente.')
    end
    send(c,res_str)
  elseif err == 'closed' then
    --remove a conexao dos arrays
    print('connection '..i..' '..err)
    table.remove(tab_sock,i)
    map_serv[c] = nil
    inc = 0
  end
  return i+inc
end

--funcoes exportadas

function createproxy (IP, port, interface,close)
  --print('createproxy',IP, port)
  return setmetatable({},{__index = create_index_func(IP, port, interface,close)})
end

function setServIp (IP)
  serv_ip = IP
end

function createServant (obj, interface) 
  assert(serv_ip,"IP local não especificado.")
  local sock,err = assert(socket.bind(serv_ip, free_port))
  local info = {ip=serv_ip,port=free_port}
  free_port = free_port+1
  map_serv[sock] = obj
  map_itf[obj] = interface
  table.insert(tab_sock,sock)
  sock:setoption('tcp-nodelay',true)
  return info
end

function waitIncomming ()
  n_servs = #tab_sock
  print("waitIncomming",n_servs..' servants')
  for i,s in ipairs(tab_sock) do
    check_servant(s,i)
  end
  while true do
    print('\nselect-------------------------------------')
    socket.select(tab_sock)
    local i=#tab_sock
    while tab_sock[i] do
      if i>n_servs then 
        i=check_connection(tab_sock[i],i)
      else
        i=check_servant(tab_sock[i],i)
      end
    end
  end
end

-- bug: o string 'nil' atualmente nao pode ser passado como parametro ou
-- como retorno porque sera substituido por nil na hora do marshalin/unmarshaling
